//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public import LanguageServerProtocol

/// The build target sources request is sent from the client to the server to
/// query for the list of text documents and directories that belong to a
/// build target. The sources response must not include sources that are
/// external to the workspace.
public struct BuildTargetSourcesRequest: RequestType, Hashable {
  public static let method: String = "buildTarget/sources"
  public typealias Response = BuildTargetSourcesResponse

  public var targets: [BuildTargetIdentifier]

  public init(targets: [BuildTargetIdentifier]) {
    self.targets = targets
  }
}

public struct BuildTargetSourcesResponse: ResponseType, Hashable {
  public var items: [SourcesItem]

  public init(items: [SourcesItem]) {
    self.items = items
  }
}

public struct SourcesItem: Codable, Hashable, Sendable {
  public var target: BuildTargetIdentifier

  /// The text documents and directories that belong to this build target.
  public var sources: [SourceItem]

  /// The root directories from where source files should be relativized.
  /// Example: ["file://Users/name/dev/metals/src/main/scala"]
  public var roots: [URI]?

  public init(target: BuildTargetIdentifier, sources: [SourceItem], roots: [URI]? = nil) {
    self.target = target
    self.sources = sources
    self.roots = roots
  }
}

public struct SourceItem: Codable, Hashable, Sendable {
  /// Either a text document or a directory. A directory entry must end with a
  /// forward slash "/" and a directory entry implies that every nested text
  /// document within the directory belongs to this source item.
  public var uri: URI

  /// Type of file of the source item, such as whether it is file or directory.
  public var kind: SourceItemKind

  /// Indicates if this source is automatically generated by the build and is
  /// not intended to be manually edited by the user.
  public var generated: Bool

  /// Kind of data to expect in the `data` field. If this field is not set, the kind of data is not specified.
  public var dataKind: SourceItemDataKind?

  /// Language-specific metadata about this source item.
  public var data: LSPAny?

  /// If `dataKind` is `sourceKit`, the `data` interpreted as `SourceKitSourceItemData`, otherwise `nil`.
  public var sourceKitData: SourceKitSourceItemData? {
    guard dataKind == .sourceKit else {
      return nil
    }
    return SourceKitSourceItemData(fromLSPAny: data)
  }

  public init(
    uri: URI,
    kind: SourceItemKind,
    generated: Bool,
    dataKind: SourceItemDataKind? = nil,
    data: LSPAny? = nil
  ) {
    self.uri = uri
    self.kind = kind
    self.generated = generated
    self.dataKind = dataKind
    self.data = data
  }
}

public enum SourceItemKind: Int, Codable, Hashable, Sendable {
  /// The source item references a normal file.
  case file = 1

  /// The source item references a directory.
  case directory = 2
}

public struct SourceItemDataKind: RawRepresentable, Codable, Hashable, Sendable {
  public var rawValue: String

  public init(rawValue: String) {
    self.rawValue = rawValue
  }

  /// `data` field must contain a JvmSourceItemData object.
  public static let jvm = SourceItemDataKind(rawValue: "jvm")

  /// `data` field must contain a `SourceKitSourceItemData` object.
  ///
  /// **(BSP Extension)**
  public static let sourceKit = SourceItemDataKind(rawValue: "sourceKit")
}

/// **(BSP Extension)**

public enum SourceKitSourceItemKind: String, Codable {
  /// A source file that belongs to the target
  case source = "source"

  /// A header file that is clearly associated with one target.
  ///
  /// For example header files in SwiftPM projects are always associated to one target and SwiftPM can provide build
  /// settings for that header file.
  ///
  /// In general, build systems don't need to list all header files in the `buildTarget/sources` request: Semantic
  /// functionality for header files is usually provided by finding a main file that includes the header file and
  /// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
  /// semantic functionality for header files if they haven't been included by any main file.
  case header = "header"

  /// A SwiftDocC documentation catalog usually ending in the ".docc" extension.
  case doccCatalog = "doccCatalog"
}

public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
  /// The language of the source file. If `nil`, the language is inferred from the file extension.
  public var language: Language?

  /// The kind of source file that this source item represents. If omitted, the item is assumed to be a normal source file,
  /// ie. omitting this key is equivalent to specifying it as `source`.
  public var kind: SourceKitSourceItemKind?

  /// The output path that is used during indexing for this file, ie. the `-index-unit-output-path`, if it is specified
  /// in the compiler arguments or the file that is passed as `-o`, if `-index-unit-output-path` is not specified.
  ///
  /// This allows SourceKit-LSP to remove index entries for source files that are removed from a target but remain
  /// present on disk.
  ///
  /// The server communicates during the initialize handshake whether it populates this property by setting
  /// `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
  public var outputPath: String?

  public init(language: Language? = nil, kind: SourceKitSourceItemKind? = nil, outputPath: String? = nil) {
    self.language = language
    self.kind = kind
    self.outputPath = outputPath
  }

  public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
    if case .string(let language) = dictionary[CodingKeys.language.stringValue] {
      self.language = Language(rawValue: language)
    }
    if case .string(let rawKind) = dictionary[CodingKeys.kind.stringValue] {
      self.kind = SourceKitSourceItemKind(rawValue: rawKind)
    }
    // Backwards compatibility for isHeader
    if case .bool(let isHeader) = dictionary["isHeader"], isHeader {
      self.kind = .header
    }
    if case .string(let outputFilePath) = dictionary[CodingKeys.outputPath.stringValue] {
      self.outputPath = outputFilePath
    }
  }

  public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny {
    var result: [String: LSPAny] = [:]
    if let language {
      result[CodingKeys.language.stringValue] = .string(language.rawValue)
    }
    if let kind {
      result[CodingKeys.kind.stringValue] = .string(kind.rawValue)
    }
    if let outputPath {
      result[CodingKeys.outputPath.stringValue] = .string(outputPath)
    }
    return .dictionary(result)
  }
}
